﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Timers;
using Samurai.Wakizashi.Concurrency;
using Timer = System.Timers.Timer;

namespace Samurai.Wakizashi.Caching
{
    public class ExpiringKeyedCache : DisposableBase, IKeyedCacheStore<string, object>, IExpiringCacheStore
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="ExpiringKeyedCache"/> class.
        /// </summary>
        /// <param name="expirationInterval">The expiration interval.</param>
        public ExpiringKeyedCache(TimeSpan expirationInterval)
        {
            ExpirationTimeSpan = expirationInterval;
        }

        #region Fields

        private readonly Dictionary<string, object> _items = new Dictionary<string, object>();
        private readonly ReaderWriterLock _readerWriterLock = new ReaderWriterLock();
        private Timer _expirationTimer;

        #endregion

        #region Implementation of IExpiringCacheStore

        /// <summary>
        /// Gets or sets the expiration time span.
        /// </summary>
        /// <value>The expiration time span.</value>
        public TimeSpan ExpirationTimeSpan { get; set; }

        /// <summary>
        /// Gets or sets the action to execute upon cache expiration.
        /// </summary>
        /// <value>The action to execute.</value>
        public Action<IExpiringCacheStore> ExpireAction { get; set; }

        #endregion

        #region Implementation of IKeyedCacheStore

        /// <summary>
        /// Caches the provided item an associates it with the specified key.
        /// </summary>
        /// <param name="key">The key.</param>
        /// <param name="item">The item.</param>
        public void CacheItem(string key, object item)
        {
            using (new WriterLock(_readerWriterLock, TimeSpan.FromSeconds(5)))
            {
                if (_items.ContainsKey(key))
                    _items[key] = item;
                else
                    _items.Add(key, item);

                if(_items.Count == 1) 
                    BeginExpirationClock();
            }
        }

        public void CacheItems(IEnumerable<object> items, Func<object, string> keyLocatorExpression)
        {
            using (new WriterLock(_readerWriterLock, TimeSpan.FromSeconds(25)))
            {
                foreach (var item in items)
                {
                    var key = keyLocatorExpression.Invoke(item);
                    if (_items.ContainsKey(key))
                        _items[key] = item;
                    else
                        _items.Add(key, item);
                }
            }
        }

        /// <summary>
        /// Evicts the item from the cache.
        /// </summary>
        /// <param name="key">The key.</param>
        public void EvictItem(string key)
        {
            using (new WriterLock(_readerWriterLock, TimeSpan.FromSeconds(5)))
            {
                if (_items.ContainsKey(key))
                    _items.Remove(key);
            }
        }

        /// <summary>
        /// Clears all items from the cache.
        /// </summary>
        public void Clear()
        {
            using (new WriterLock(_readerWriterLock, TimeSpan.FromSeconds(5)))
            {
                _items.Clear();
            }
        }

        /// <summary>
        /// Gets the item located at the specified key.
        /// </summary>
        /// <param name="key">The key.</param>
        /// <returns>The item requested or <c>null</c> if none is found.</returns>
        public object GetItem(string key)
        {
            using (new ReaderLock(_readerWriterLock, TimeSpan.FromSeconds(5)))
            {
                object value;
                return _items.TryGetValue(key, out value) ? value : null;
            }
        }

        /// <summary>
        /// Gets the item at the specified key and attempts to cast it to the specified type.
        /// </summary>
        /// <typeparam name="TCast">The type of the cast.</typeparam>
        /// <param name="key">The key.</param>
        /// <returns>The requested item. If none is found, a <c>null</c> reference is returned.</returns>
        public TCast GetItemAs<TCast>(string key)
        {
            var item = GetItem(key);
            if (item == null) return default(TCast);

            return (TCast) item;
        }

        /// <summary>
        /// Gets all items held in the cache.
        /// </summary>
        /// <value>All items in the cache.</value>
        public IEnumerable<object> AllItems 
        {
            get
            {
                using (new ReaderLock(_readerWriterLock, TimeSpan.FromSeconds(5)))
                {
                    return (from k in _items.ToArray()
                            select k.Value).ToArray();   
                }
            }
        }

        #endregion

        protected void ExpirationTimer_Elapsed(object sender, ElapsedEventArgs e)
        {
            _expirationTimer.Stop();
            _expirationTimer.Elapsed -= ExpirationTimer_Elapsed;
            _expirationTimer.Dispose();

            Clear();

            if (ExpireAction != null)
                ExpireAction.Invoke(this);
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Timer will be disposed when either restarting or when this instance is disposed. No reason to dispose within this method.")]
        private void BeginExpirationClock()
        {
            if (ExpirationTimeSpan == TimeSpan.MinValue)
                ExpirationTimeSpan = TimeSpan.FromMinutes(1);

            _expirationTimer = new Timer
            {
                Interval = ExpirationTimeSpan.TotalMilliseconds
            };
            _expirationTimer.Elapsed += ExpirationTimer_Elapsed;
            _expirationTimer.Start();
        }

        protected override void Dispose(bool disposing)
        {
            if (_expirationTimer != null)
                _expirationTimer.Dispose();

            base.Dispose(disposing);
        }
    }
}
